import co from 'co';
import amqp from 'amqplib';


const DEFAULT_OPTS = {
    exchange: {
        name: '',
        type: 'topic',
        opts: {
            durable: false,
        },
    },
    pattern: '',
    queue: {
        name: '',
        opts: {
            durable: false,
        },
    },
    consume: {
        opts: {},
    },
};

const SEND_CH_NAME = '__SEND__';


/**
 * KoaMqReceiver
 */
class KoaMqReceiver {
    constructor(app, router, opts) {
        const DEFAULTS = {};

        this.options = Object.assign({}, DEFAULTS, opts);

        this.app = app;

        this.log = this.app.log.getLogger('mq');

        this.router = router;

        this.conn = null;

        this.methods = {};

        this.channels = {};

        // methods
        this.connect = co.wrap(this.connectGen);
        this.createSendChannel = co.wrap(this.createSendChannelGen);
        this.createChannel = co.wrap(this.createChannelGen);
    }

    * connectGen(uri, done) {
        try {
            this.conn = yield amqp.connect(uri);

            this.register.call(this);

            if (done) done();
        } catch (e) {
            this.log.error('connect failed: %s', e);
        }
    }

    register() {
        const routes = this.router.routes();

        this.createSendChannel();

        for (const [name, item] of Object.entries(routes)) {
            const opts = item.opts;
            const gen = item.gen;
            this.methods[name] = co.wrap(gen);

            this.createChannel(name, opts);
        }
    }

    * createSendChannelGen() {
        try {
            const name = SEND_CH_NAME;
            const ch = yield this.conn.createChannel();

            // binds
            ch.on('close', this.onChannelClose.bind(this, name));
            ch.on('error', this.onChannelError.bind(this, name));
            ch.on('return', this.onChannelReturn.bind(this, name));
            ch.on('drain', this.onChannelDrain.bind(this, name));

            this.log.info('send channel ready!');

            this.addChannel(name, ch);
        } catch (e) {
            this.log.error('send channel create failed: %s', e);
        }
    }

    * createChannelGen(name, opts = {}) {
        const options = Object.assign({}, DEFAULT_OPTS, opts);

        try {
            const ch = yield this.conn.createChannel();

            // binds
            ch.on('close', this.onChannelClose.bind(this, name));
            ch.on('error', this.onChannelError.bind(this, name));
            ch.on('return', this.onChannelReturn.bind(this, name));
            ch.on('drain', this.onChannelDrain.bind(this, name));

            let ok;

            // asserts
            ok = yield ch.assertExchange(
                options.exchange.name,
                options.exchange.type,
                options.exchange.opts
            );

            this.log.info('channel[%s] exchange ok! %s, %s',
                name, JSON.stringify(ok), JSON.stringify(options.exchange));

            ok = yield ch.assertQueue(
                options.queue.name,
                options.queue.opts
            );

            this.log.info('channel[%s] queue ok! %s, %s',
                name, JSON.stringify(ok), JSON.stringify(options.queue));


            ok = yield ch.bindQueue(
                options.queue.name,
                options.exchange.name,
                options.pattern
            );

            this.log.info('channel[%s] bind ok! %s, bind with %s',
                name, JSON.stringify(ok), JSON.stringify(options.pattern));

            const context = {
                app: this.app,
                channel: ch,
                name,
                options,
            };

            ok = yield ch.consume(
                options.queue.name,
                this.methods[name].bind(context),
                options.consume.opts
            );

            this.log.info('channel[%s] ready for incomming message! %s, %s',
                name, JSON.stringify(ok), JSON.stringify(options.consume));

            this.addChannel(name, ch);
        } catch (e) {
            this.log.error('channel[%s] create failed: %s', name, e);
        }
    }

    onChannelClose(name) {
        this.log.warn('channel[%s] close', name);

        delete this.channels[name];

        if (name === SEND_CH_NAME) {
            this.createSendChannel();
        }
    }

    onChannelError(name, err) {
        this.log.warn('channel[%s] error: %s', name, err);
    }

    onChannelReturn(name, msg) {
        this.log.warn('channel[%s] close: %s', name, msg);
    }

    onChannelDrain(name) {
        this.log.warn('channel[%s] close', name);
    }

    getConnection() {
        return this.conn;
    }

    addChannel(name, ch) {
        this.channels[name] = ch;
    }

    getChannel(name) {
        if (name && name in this.channels) {
            return this.channels[name];
        }
        return null;
    }

    getSendChannel() {
        if (SEND_CH_NAME in this.channels) {
            return this.channels[SEND_CH_NAME];
        }
        return null;
    }
}

export default KoaMqReceiver;
